JS - Geordende tabellen
Home

JS - Geordende tabellen

JS - Geordende tabellen

  1. We maken het mogelijk om tabellen de ordenen op de inhoud van één bepaalde kolom.
  2. Je kan per kolom aangeven als de ordening dalend of stijgend moet zijn.
  3. Die functionaliteit moet aan meer dan één tabel kunnen toegevoegd worden door middel van slechts één instructie.
  4. Die functionaliteit moet zowel via het toetsenbord als via de muis toegankelijk zijn.
  5. De code vind je terug op Bitbucket.

Video

Constructor functie

Om de code te kunnen toepassen op om het even welke tabel moeten we een constructor-functie schrijven (zie JS - Object Constructor). Van deze functie maken voor elke tabel, die we sorteerbaar willen maken, een instantie:

function TableSort(id, continentalNotation = true) {
    // When calling an object constructor or any of its methods,
    // this’ refers to the instance of the object
    // much like any class-based language
    this.continentalNotation = continentalNotation;
    this.tableElement = document.getElementById(id);
    if (this.tableElement && this.tableElement.nodeName == "TABLE") {
        this.prepare();
    }
}

De code hierboven maakt een constructor-functie met de naam TableSort. Met erop dat de naam van deze functie in pascal-notatie geschreven en niet in camelcase zoals dat de gewoonte is voor gewone functies.

Wanneer je de functie oproept geef je de id-selector naam mee van de tabel. De functie gebruikt de getElementId DOM methode om een referentie naar de tabel op te halen. Die referentie wordt opgeslagen in this.tableElement. Het sleutelwoord this verwijst hier naar instantie van de constructor-functie die met het new sleutelwoord gemaakt zal worden. De code test als de opgegeven id-selector overeenkomt met een table element en indien dat zo is wordt de prepare methode uitgevoerd. Deze methode doet het werk om de tabel sorteerbaar te maken. Het sorteren zelf gebeurt pas als de gebruiker op één van de koppen van de tabel klikt.

Met de tweede parameter geef je aan in welke notatie de getallen geschreven zijn. In de Engelse notatie worden kommagetallen gescheiden door een punt en duizenden door een komma. In de continentale schrijfwijze is het precies omgekeerd. Onze sort methode moet dus rekening kunnen houden met de manier waarop de getallen geschreven worden. We declareren daarvoor een eigenschap this.continentalNotation en stellen die standaar in op true.

De TableSort roepen we pas op als de HTML volledig in de browser geladen is. Dat doen we bijvoorbeeld wanneer de browser het onload event afvuurt:

window.onload = function() {
    var jommeke = new TableSort("jommeke");
    var fruit = new TableSort("fruit");
}

Kolomkoppen toegankelijk maken

  1. prepare methode
    1. De prepare methode voegen we aan onze functie toe met behulp van de prototype eigenschap ervan (zie hiervoor JS - Objecten - Prototype). De prototype eigenschap is een object dat het toevoegen van een eigenschap of een methode aan alle instanties van een constructor-functie eenvoudig maakt:
      TableSort.prototype.prepare = function() {
         ...
      }
    2. In de prepare methode beginnen we met aan te geven dat de sorteervolgorde standaard stijgend is. Daarvoor voegen we aan elke kolomkop een pijltje naar boven toe. We gaan ervan uit dat als de gebruiker voor de eerste keer op een kolomkop klikt de kolom in stijgende volgorde gesorteerd zal worden.
      Om te onthouden in welke volgorde de kolom gesorteerd zal worden wanneer de gebruiker op de kolomkop stellen we de klassennaam van het th element, die het sort event zal afvuren, in op asc. Om dit te realiseren:
      1. maken we twee CSS klassen en gebruiken de ::after selector om een pijltje achter de koptekst van de kolom toe te voegen. De klasse asc voor een pijltje naar boven en desc voor een pijltje naar beneden:
        /* css entities: https://www.w3schools.com/cssref/css_entities.asp */
        .asc::after {
            content: "\0020\0020\0020\2191";
        }
        
        .desc::after {
            content: "\0020\0020\0020\2193";
        }
      2. doorlopen we de headings htmlcollectie met een for lus en stellen de we css klasse van alle th's in op asc:
        TableSort.prototype.prepare = function () {
            // add arrow up
            // default is ascending order
            let headings = this.tableElement.tHead.rows[0].cells;
            // headings is een htmlcollection
            for (let i = 0; i < headings.length; i++) {
                headings[i].className = 'asc';
            }
        }
      3. Tenslotte voegen we wat CSS toe om de intentie van UI duidelijker te maken. Als de gebruiker over de kolomkop zweeft met de muis wordt die in reverse getoond:
        th:hover {
            cursor: hand;
            cursor: pointer;
            color: white;
            background-color: #630;
        }
    3. Nadat we het pijltje met behulp van de asc klasse aan de header hebben toegevoegd, moeten we nu een eventhandelaar toekennen die zal worden uitgevoerd als de gebruiker op een kolomkop van een tabel klikt. We koppelen de eventafhandelaar aan het click event van de tabel.
      Ons eerste idee is om de eventafhandelaar met de volgende code toe te voegen
      TableSort.prototype.prepare = function () {
          // add arrow up
          // default is ascending order
          let headings = this.tableElement.tHead.rows[0].cells;
          // headings is een htmlcollection
          for (let i = 0; i < headings.length; i++) {
              headings[i].className = 'asc';
          }
          this.tableElement.addEventListener("click", this.eventHandler, false);
      }
      
      Maar als je goed nadenkt zie je dat hier een probleem is. Je zou denken dat de this vóór de eventHandler verwijst naar de instantie van TableSort omdat je in TableSort bezig bent te coderen. Maar vergeet niet dat een eventafhandelaar eigenlijk een callback functie is. De code this.eventHandler wordt niet afgevuurd op het moment dat de EventListener wordt toegevoegd maar op het moment dat het event wordt afgevuurd. Op dat moment verwijst this naar de kopkolom waarop geklikt werd en zal this dus verwijzen naar die kopkolom. En het kopkolom object beschikt natuurlijk niet over de methode sortColumn. Dus op het moment dat we de EventListener toevoegen moeten we een verwijzing naar de this van het SortTable object onthouden. Dat doen we met behulp van een closure (zie Closures en Closures in de praktijk):
      TableSort.prototype.prepare = function () {
          // add arrow up
          // default is ascending order
          let headings = this.tableElement.tHead.rows[0].cells;
          // headings is een htmlcollection
          for (let i = 0; i < headings.length; i++) {
              headings[i].className = 'asc';
          }
          
          this.tableElement.addEventListener("click", function (that) {
              return function (event) {
                  that.eventHandler(event);
              }
          }(this), false);
      }
      We hebben hier wel een mooi voorbeeld van het gevolg dat functies in JavaScript eersteklasrangburgers zijn:
      1. anonieme functie: in de eventHandler geven we een anonieme functie mee die een anonieme functie retourneert
      2. closure: de buitenste anonieme functie maakt een closure die ervoor zorgt dat de referentie naar het object TableSort bij het afvuren van het klik event bekend is;
      3. een IIFE of Immediately-Invoked Function Expression: bij het toekennen van de anonieme functie aan de eventhandler wordt die anonieme functie onmiddelijk uitgevoerd, dat gebeurt dus tijdens het toekennen van de functie aan de eventhandler en niet op het moment dat gebruikt op de kolomkop klikt; dat doen we omdat we de referentie naar de this van TableSort met behulp van een closure willen meegeven aan de eventhandler die slechts zal worden uitgevoerd op het moment dat erop de kolomkop wordt geklikt;
  2. eventHandler methode
    De gebruiker moet kunnen aangeven dat de tabel geordend moet worden met behulp van het toetsenbord en van de muis. Toegankelijk voor de gebruiker wil zeggen dat we door de gebruiker afgevuurde events moeten kunnen ondervangen. We gebruiken hiervoor:
    1. addEventListener van de DOM;
    2. de opgaande stroom van de events (zie hiervoor JS - Eventstroom): we gaan niet aan elke kolomkop afzonderlijk een eventafhandelaar toekennen.

      We laten de afgevuurde events opborrelen tot aan het table element en pas aan het table element kennen we een EventListener toe die zal nagaan welk element in de tabel het event heeft afgevuurd. Op die manier houden we alle logica die te maken heeft met de interactie met de gebruiker in één overzichtelijke methode.

      Als het event werd afgevuurd door op een th element te klikken wordt de sorteer methode uitgevoerd. Om te verifiëren als erop een th is geklikt schrijven we het volgende statement:

      if (event.target.tagName === 'TH') {
          // alert('kolomkop');
          this.sortColumn(event.target);
          ...
      }

      Maar als er een nog een ander HTML element in het th element staat, in de kopteksten van de suiker tabel staat bijvoorbeeld <th><strong></strong>Product</strong></th>, gaat de sort mehode niet uitgevoerd worden. Want er wordt niet op een th element geklikt maar op een strong element. Daarom gaan we zoeken naar het eerst bovenliggende th element van het element waarop geklikt werd, in ons voorbeeld een strong element. We gebruiken daarvoor de event.target.closest('TH') methode. Meer info hierover vind je op Detecting clicks inside an element with vanilla JavaScript.

    TableSort.prototype.eventHandler = function (event) {
        // zoek het eerst bovenliggende TH element en sla het op in een variabele
        let elemTarget = event.target.closest('TH');
        if (elemTarget.tagName === 'TH') {
            // alert('kolomkop');
            // sorteer eerst in de richting van de pijl
            this.sortColumn(elemTarget);
            // draai de pijl om om de richting van de volgende
            // sort te bepalen als de gebruiker weer klikt op de kolomkop
            if (elemTarget.className === "asc") {
                elemTarget.className = 'desc';
            } else {
                elemTarget.className = 'asc';
            }
        }
    }

Het sorteer algoritme

Op het moment dat de gebruiker op een kolomkop klikt moet de tabel geordend worden op deze kolom. De eventHandler ondervangt dit klik-event en voert de sortColomn methode uit.

  1. Onthouden in welke volgorde de kolom gesorteerd is. Daarvoor hallen we de klassennaam op van het th element waarop geklikt werd.
  2. Initialiseer enkele variabelen:
    1. rows: deze variabele gebruiken als een shortcut die verwijst naar de tabelrijen, op die manier moeten we niet altijd de volledige this.tableElement.rows intypen
    2. alpha, numeric: in deze arrays slaan we alfanumerieke en numerieke inhoud van de cellen in de tabel op en de rijindex van de cel in een literal array van de vorm:
      {
         value: 'de inhoud van cel'
         row: index van de rij waarin de cel staat
      }
    3. aIndex, nIndex: deze variabelen gebruiken we als indices, telkens als we een element aan een van beide array's toevoegen wordt die met 1 vermeerderd
    4. cellIndex: in deze variabele zetten we cellIndex waarde van de kopkolom waarop geklikt werd. cellIndex is een eigenschap van het col DOM element en retourneert de plaats van de kolom in de rij waarin die zich bevindt.
    TableSort.prototype.sortColumn = function(headerCell) {
        // onthouden in welke volgorde de kolom nu geordend is
    	const order = headerCell.className;
    
        // Get cell data for column that is to be sorted from HTML table
        let rows = this.tableElement.rows;
        let alpha = [],
            numeric = [];
        let alphaIndex = 0,
            numericIndex = 0;
        let cellIndex = headerCell.cellIndex;
        .
        .
        .
    }
  3. Vervolgens doorlopen we elke rij van de tabel en bewerken de cel die onder de kolomkop valt waarop de gebruiker geklikt heeft. We willen alleen de tekst uit de cel halen en de eventuele HTML erin negeren. Daarvoor gebruiken we de textContent of innerText eigenschap. Firefox ondersteunt alleen textContent en IE alleen innerText. Andere browsers ondersteunen beiden. We gebruiken daarom de ternaire operator die de een of de andere eigenschap gebruikt afhankelijk van welke eigenschap in de browser voorhanden is.
    TableSort.prototype.sortColumn = function(headerCell) {
        ...
        for (let i = 1; rows[i]; i++) {
            let cell = rows[i].cells[cellIndex];
            let content = cell.textContent ? cell.textContent : cell.innerText;
            alert(content)
        }
    }
  4. We moeten kunnen bepalen of de cel een numerieke waarde bevat of niet:
    1. Daarvoor moeten we eerst alle karakters verwijderen die ervoor kunnen zorgen dat de celinhoud geïnterpreteerd wordt als alfanumeriek zoals het euroteken of dollarteken enz. We verwijderen alle alfakaracters en wijzigen Europese notatie in Engelse.
      Eventuele verbetering: aan de constructor meegeven of Engelse of contintentale notatie gebruikt wordt.
      Dat doen we met een regulieren expressie (zie meer daarover op reguliere expressies).
    2. Met de parseFloat functie gaan we na of de uitgezuiverde waarde een getal is of niet.
    3. Indien het een getal is slaan we uitgezuiverde waarde en de referentie naar de rij waarin de cel staat, als een literal array, in de numeric array (in de laatste wijziging beschouwen we de tekst in de cel als getal als er cijfers in staan, de alpha tekst wordt verwijderd)
    4. anders in de alpha array.
    5. de literal array, de uitgezuiverde waarde en de referentie naar de rij waarin de betreffende kolom staat, hebben we later nodig om de geordende tabel weer op te bouwen;
    TableSort.prototype.sortColumn = function(headerCell) {
        // onthoudt in welke volgorde de kolom nu geordend is
        const order = headerCell.className;
        // Get cell data for column that is to be sorted from HTML table
        let rows = this.tableElement.rows;
        let alpha = [],
            numeric = [];
        let alphaIndex = 0,
            numericIndex = 0;
        let cellIndex = headerCell.cellIndex;
        for (var i = 1; rows[i]; i++) {
            let cell = rows[i].cells[cellIndex];
            let content = cell.textContent ? cell.textContent : cell.innerText;
    
    
            // let numericValue = content.replace(/(\€\$|\,|\s)/g, "");
            // als er getallen instaan, verwijder alle karakters die geen getallen,
            // punt of komma zijn
            let numericValue;
            if (this.continentalNotation) {
                // vervang vervolgens komma door punt en punt door komma
                numericValue = content.replace(/[^0-9,.]*/g, "").replace(/[,.]/g, function (m) {
                    // m is the match found in the string
                    // If `,` is matched return `.`, if `.` matched return `,`
                    return m === ',' ? '.' : ',';
                });
            } else {
                numericValue = content.replace(/[^0-9,.]*/g, "");           
            }
    
            // alert(numericValue);
            if (parseFloat(numericValue) == numericValue) {
                numeric[numericIndex++] = {
                    value: Number(numericValue),
                    row: rows[i]
                }
            }
            else {
                alpha[alphaIndex++] = {
                    value: content,
                    row: rows[i]
                }
            }
        }
    }
  5. De sort implementeren

    We gaan niet zelf een sorteeralgoritme schrijven maar de sort methode van het Array object gebruiken (zie JS - array methoden - muterend). We gebruiken de order variabele om te kijken als we in dalende of stijgende volgorde moeten sorteren.
    Voeg de volgende code toe aan het einde van de SortColumn methode van hierboven:

    TableSort.prototype.sortColumn = function (headerCell) {
        // onthoudt in welke volgorde de kolom nu geordend is
        const order = headerCell.className;
        // alert(order);
        // Get cell data for column that is to be sorted from HTML table
        let rows = this.tableElement.rows;
        let alpha = [],
            numeric = [];
        let alphaIndex = 0,
            numericIndex = 0;
        let cellIndex = headerCell.cellIndex;
        for (let i = 1; rows[i]; i++) {
            let cell = rows[i].cells[cellIndex];
            let content = cell.textContent ? cell.textContent : cell.innerText;
    
            // let numericValue = content.replace(/(\€\$|\,|\s)/g, "");
            // als er getallen instaan, verwijder alle karakters die geen getallen,
            // punt of komma zijn
            let numericValue;
            if (this.continentalNotation) {
                // vervang vervolgens komma door punt en punt door komma
                numericValue = content.replace(/[^0-9,.]*/g, "").replace(/[,.]/g, function (m) {
                    // m is the match found in the string
                    // If `,` is matched return `.`, if `.` matched return `,`
                    return m === ',' ? '.' : ',';
                });
            } else {
                numericValue = content.replace(/[^0-9,.]*/g, "");           
            }
    
            // alert(numericValue);
            if (parseFloat(numericValue) == numericValue) {
                numeric[numericIndex++] = {
                    value: Number(numericValue),
                    row: rows[i]
                }
            }
            else {
                alpha[alphaIndex++] = {
                    value: content,
                    row: rows[i]
                }
            }
        }
    
        numeric.sort(function (a, b) {
            if (order === 'asc') {
                return a.value - b.value;
            } else {
                return b.value - a.value;
            }
        });
    
        alpha.sort(function (a, b) {
            let aName = a.value.toLowerCase();
            let bName = b.value.toLowerCase();
            if (aName < bName) {
                if (order === 'asc') {
                    return -1;
                } else {
                   return 1
                }            
            }
            else if (aName > bName) {
                if (order === 'asc') {
                    return 1;
                } else {
                   return -1
                }            
           }
            else {
                return 0;
            }
        });
    }
    
    
  6. De geordende tabel opnieuw genereren

    Tensolotte moeten we de tabel opnieuw opbouwen maar nu gesorteerd op de inhoud van de geselecteerde kolom. Voeg daarvoor de volgende code toe aan het einde van de SortColumn methode van hierboven:

    TableSort.prototype.sortColumn = function (headerCell) {
        // onthoudt in welke volgorde de kolom nu geordend is
        const order = headerCell.className;
        // alert(order);
        // Get cell data for column that is to be sorted from HTML table
        let rows = this.tableElement.rows;
        let alpha = [],
            numeric = [];
        let alphaIndex = 0,
            numericIndex = 0;
        let cellIndex = headerCell.cellIndex;
        for (let i = 1; rows[i]; i++) {
            let cell = rows[i].cells[cellIndex];
            let content = cell.textContent ? cell.textContent : cell.innerText;
    
            // let numericValue = content.replace(/(\€\$|\,|\s)/g, "");
            // als er getallen instaan, verwijder alle karakters die geen getallen,
            // punt of komma zijn
            let numericValue;
            if (this.continentalNotation) {
                // vervang vervolgens komma door punt en punt door komma
                numericValue = content.replace(/[^0-9,.]*/g, "").replace(/[,.]/g, function (m) {
                    // m is the match found in the string
                    // If `,` is matched return `.`, if `.` matched return `,`
                    return m === ',' ? '.' : ',';
                });
            } else {
                numericValue = content.replace(/[^0-9,.]*/g, "");           
            }
    
            // alert(numericValue);
            if (parseFloat(numericValue) == numericValue) {
                numeric[numericIndex++] = {
                    value: Number(numericValue),
                    row: rows[i]
                }
            }
            else {
                alpha[alphaIndex++] = {
                    value: content,
                    row: rows[i]
                }
            }
        }
    
        numeric.sort(function (a, b) {
            if (order === 'asc') {
                return a.value - b.value;
            } else {
                return b.value - a.value;
            }
        });
    
        alpha.sort(function (a, b) {
            let aName = a.value.toLowerCase();
            let bName = b.value.toLowerCase();
            if (aName < bName) {
                if (order === 'asc') {
                    return -1;
                } else {
                   return 1
                }            
            }
            else if (aName > bName) {
                if (order === 'asc') {
                    return 1;
                } else {
                   return -1
                }            
           }
            else {
                return 0;
            }
        });
    
        let orderdedColumns = [];
        orderdedColumns = numeric.concat(alpha);
        let tBody = this.tableElement.tBodies[0];
        for (let i = 0; orderdedColumns[i]; i++) {
            tBody.appendChild(orderdedColumns[i].row);
        }
    }

    En hiermee is de sortColumn methode volledig afgewerkt.

Een JS library bestand maken

Tenslote halen we de JavaScript code voor het ordenen van een tabel uit de HTML en plaatsen het in een apart bestand met de naam tablesort.js in de map js.

De HTML

Als voorbeeld nemen we enkele Jommeke's albums en een tabel die aangeeft hoeveel calorieën er in 100 g fruit zit. We gebruiken twee tabellen omdat onze sorteerfunctie op meer dan één tabel op dezelfde pagina toegepast moet kunnen worden:

<table id="jommeke" class="spreadsheet">
    <caption>Jommeke albums</caption>
    <col />
    <col />
    <col />
    <col />
    <col />
    <col />
    <thead>
        <tr>
            <th scope="col">Nummer</th>
            <th scope="col">Titel</th>
            <th scope="col">Kaft</th>
            <th scope="col">&euro;</th>
            <th scope="col">&yen;</th>
            <th scope="col">&pound;</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">1</th>
            <td>Jacht op een voetbal</td>
            <td>Softcover</td>
            <td>5,22</td>
            <td>34</td>
            <td>3,76</td>
        </tr>
        <tr>
            <th scope="row">2</th>
            <td>De zingende aap</td>
            <td>Softcover</td>
            <td>5,22</td>
            <td>34</td>
            <td>3,76</td>
        </tr>
        <tr>
            <th scope="row">3</th>
            <td>De Koningin van Onderland</td>
            <td>Hardcover</td>
            <td>8,22</td>
            <td>54,1</td>
            <td>5,91</td>
        </tr>
        <tr>
            <th scope="row">4</th>
            <td>Purperen pillen</td>
            <td>Softcover</td>
            <td>5,22</td>
            <td>34</td>
            <td>3,76</td>
        </tr>
        <tr>
            <th scope="row">5</th>
            <td>De Muzikale Bella</td>
            <td>Hardcover</td>
            <td>8,22</td>
            <td>54,1</td>
            <td>5,91</td>
        </tr>
    </tbody>
</table>

De calorieëntabel voor fruit:

<table id="fruit" class="spreadsheet">
    <thead>
        <tr>
            <th>Fruitsoort</th>
            <th>hoeveelheid</th>
            <th>calorieën</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>abrikoos met schil</td>
            <td>3 st. (100 gr)</td>
            <td align="right">48</td>
        </tr>
        <tr>
            <td>ananas</td>
            <td>100 gr</td>
            <td align="right">60</td>
        </tr>
        <tr>
            <td>appel met schil</td>
            <td>1 st. (100 gr)</td>
            <td align="right">52</td>
        </tr>
        <tr>
            <td>banaan</td>
            <td>1 st .</td>
            <td align="right">94</td>
        </tr>
        <tr>
            <td>blauwe bessen</td>
            <td>100 g</td>
            <td align="right">75</td>
        </tr>
        <tr>
            <td>citroen</td>
            <td>1 st.</td>
            <td align="right">17</td>
        </tr>
        <tr>
            <td>druiven</td>
            <td>100 g</td>
            <td align="right">54</td>
        </tr>
        <tr>
            <td>frambozen</td>
            <td>100 g</td>
            <td align="right">68</td>
        </tr>
        <tr>
            <td>grapefruit</td>
            <td>1 st.</td>
            <td align="right">82</td>
        </tr>
        <tr>
            <td>kersen</td>
            <td>100 g</td>
            <td align="right">64</td>
        </tr>
        <tr>
            <td>kiwi</td>
            <td>1 st</td>
            <td align="right">46</td>
        </tr>
        <tr>
            <td>nectarine</td>
            <td>1 st.</td>
            <td align="right">67</td>
        </tr>
        <tr>
            <td>peer</td>
            <td>1 st.</td>
            <td align="right">98</td>
        </tr>
        <tr>
            <td>perzik</td>
            <td>1 st.</td>
            <td align="right">42</td>
        </tr>
        <tr>
            <td>pruim</td>
            <td>1 st.</td>
            <td align="right">36</td>
        </tr>
        <tr>
            <td>sinaasappel</td>
            <td>1 st.</td>
            <td align="right">86</td>
        </tr>
        <tr>
            <td>watermeloen</td>
            <td>100 g</td>
            <td align="right">37</td>
        </tr>
    </tbody>
</table>

De suiker tabel

<table id="fruit-sugar">
    <caption>Suikerlijst fruit</caption>
    <thead>
        <tr>
            <th><strong>Product</strong></th>
            <th><strong>Per portie</strong></th>
            <th><strong>Suiker</strong></th>
            <th><strong>Energie</strong></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Aardbeien</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>5,1 g</td>
            <td>29 kcal</td>
        </tr>
        <tr>
            <td>Abrikozen, gedroogd</td>
            <td>1 stuks (= 9 g)</td>
            <td>5 g</td>
            <td>26 kcal</td>
        </tr>
        <tr>
            <td>Abrikozen, vers</td>
            <td>1 stuk (= 20 g)</td>
            <td>1,6 g</td>
            <td>9 kcal</td>
        </tr>
        <tr>
            <td>Ananas</td>
            <td>1 schaaltje (= 100&nbsp;g)</td>
            <td>11,6 g</td>
            <td>57 kcal</td>
        </tr>
        <tr>
            <td>Appel</td>
            <td>1 stuk (= 135 g)</td>
            <td>14 g</td>
            <td>81 kcal</td>
        </tr>
        <tr>
            <td>Banaan</td>
            <td>1 middel (= 130 g)</td>
            <td>20,1 g</td>
            <td>124 kcal</td>
        </tr>
        <tr>
            <td>Blauwe bessen</td>
            <td>1 schaaltje (= 100&nbsp;g)</td>
            <td>10 g</td>
            <td>52 kcal</td>
        </tr>
        <tr>
            <td>Blauwe druiven</td>
            <td>1 schaaltje (= 100&nbsp;g)</td>
            <td>16,8 g</td>
            <td>75 kcal</td>
        </tr>
        <tr>
            <td>Cranberries, gedroogd</td>
            <td>1 schaaltje (= 100&nbsp;g)</td>
            <td>64,6 g</td>
            <td>335 kcal</td>
        </tr>
        <tr>
            <td>Cranberries, vers</td>
            <td>1 schaaltje (= 100&nbsp;g)</td>
            <td>3,4 g</td>
            <td>24 kcal</td>
        </tr>
        <tr>
            <td>Dadels, gedroogd, geconfijt</td>
            <td>1 stuk (= 6 g)</td>
            <td>4,2 g</td>
            <td>19 kcal</td>
        </tr>
        <tr>
            <td>Dadels, vers</td>
            <td>1 stuk (= 6 g)</td>
            <td>1,9 g</td>
            <td>8 kcal</td>
        </tr>
        <tr>
            <td>Frambozen</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>4,5 g</td>
            <td>35 kcal</td>
        </tr>
        <tr>
            <td>Grapefruit</td>
            <td>1 stuk (= 150 g)</td>
            <td>10 g</td>
            <td>28 kcal</td>
        </tr>
        <tr>
            <td>Kaki</td>
            <td>1 stuk (= 150 g)</td>
            <td>27,9 g</td>
            <td>116 kcal</td>
        </tr>
        <tr>
            <td>Kersen</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>11,5 g</td>
            <td>54 kcal</td>
        </tr>
        <tr>
            <td>Kiwi</td>
            <td>1 stuks (= 75 g)</td>
            <td>7,7 g</td>
            <td>51 kcal</td>
        </tr>
        <tr>
            <td>Kumquat</td>
            <td>1 stuk (= 10 g)</td>
            <td>0,9 g</td>
            <td>5 kcal</td>
        </tr>
        <tr>
            <td>Lychee</td>
            <td>1 stuk (= 10 g)</td>
            <td>1,6 g</td>
            <td>7 kcal</td>
        </tr>
        <tr>
            <td>Mandarijn</td>
            <td>1 stuk (= 55 g)</td>
            <td>4,5 g</td>
            <td>25 kcal</td>
        </tr>
        <tr>
            <td>Mango</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>13,9 g</td>
            <td>66 kcal</td>
        </tr>
        <tr>
            <td>Nectarine</td>
            <td>1 stuk (= 90 g)</td>
            <td>5,9 g</td>
            <td>32 kcal</td>
        </tr>
        <tr>
            <td>Papaja</td>
            <td>1 stuk (= 100 g)</td>
            <td>7,8 g</td>
            <td>39 kcal</td>
        </tr>
        <tr>
            <td>Passievrucht</td>
            <td>1 stuk (= 15 g)</td>
            <td>0,9 g</td>
            <td>8 kcal</td>
        </tr>
        <tr>
            <td>Peer (met schil)</td>
            <td>1 stuk (= 150 g)</td>
            <td>14,2 g</td>
            <td>82 kcal</td>
        </tr>
        <tr>
            <td>Perzik</td>
            <td>1 stuk (= 110 g)</td>
            <td>8,7 g</td>
            <td>45 kcal</td>
        </tr>
        <tr>
            <td>Pruim, gedroogd</td>
            <td>1 stuk (= 8 g)</td>
            <td>3,6 g</td>
            <td>20 kcal</td>
        </tr>
        <tr>
            <td>Pruim, vers</td>
            <td>1 stuk (= 40g)</td>
            <td>2,9 g</td>
            <td>18 kcal</td>
        </tr>
        <tr>
            <td>Rozijnen, gedroogd</td>
            <td>1 handje (= 35 g)</td>
            <td>25,5 g</td>
            <td>127 kcal</td>
        </tr>
        <tr>
            <td>Sinaasappel</td>
            <td>1 stuk (= 120 g)</td>
            <td>9,2 g</td>
            <td>61 kcal</td>
        </tr>
        <tr>
            <td>Vijgen, gedroogd</td>
            <td>1 stuk (= 20 g)</td>
            <td>9,6 g</td>
            <td>52 kcal</td>
        </tr>
        <tr>
            <td>Vijgen, vers</td>
            <td>1 stuk (= 50 g)</td>
            <td>9,5 g</td>
            <td>42 kcal</td>
        </tr>
        <tr>
            <td>Watermeloen</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>8 g</td>
            <td>36 kcal</td>
        </tr>
        <tr>
            <td>Witte druiven</td>
            <td>1 schaaltje (= 100 g)</td>
            <td>15,6 g</td>
            <td>76 kcal</td>
        </tr>
    </tbody>
</table>

En de CSS om de tabel een beetje uitzicht te geven (css/tablesort.css):

table {
    border-collapse: collapse;
    border: 0.05em solid black;
    border-radius: 5em / 5em;
    background-color: #b6ff00;
}

table caption {
    font-weight: bold;
    font-size: 125%;
    text-transform: uppercase;
}

.footnote {
    font-size: 75%;
    color: #666;
}

table caption,
table th,
table td,
.footnote {
    font-family: Arial, Helvetica, sans-serif;
    padding: .5em;
}

table td {
    border: solid 1px #C35E4D;
}

table tbody th {
    color: #630;
    border-bottom: solid 1px #C35E4D;
}

table thead th {
    background: #ffe4cd ;
    border-bottom: solid 3px #E75D49;
}

/* css entities: https://www.w3schools.com/cssref/css_entities.asp */
.asc::after {
    content: "\0020\0020\0020\2191";
}

.desc::after {
    content: "\0020\0020\0020\2193";
}

table thead th:hover {
    cursor: hand;
    cursor: pointer;
    color: white;
    background-color: #630;
}

JI
2021-03-20 16:37:09